package br.usp.ime.origami.primitives;

import org.apache.log4j.Logger;

import br.usp.ime.origami.algorithms.Baskara;
import br.usp.ime.origami.foldcut.geometry.Line;
import br.usp.ime.origami.foldcut.packing.Disk;
import br.usp.ime.origami.foldcut.structures.Pair;
import br.usp.ime.origami.foldcut.structures.Triplet;
import br.usp.ime.origami.foldcut.structures.simplegraph.Vertex;

public class DiskPrimitives
{
	private static Logger logger = Logger.getLogger(DiskPrimitives.class);

	public static Disk getCircumCircles(Disk a, Disk b, Disk c)
	{

		double a2 = 2 * (a.getCenter().getX() - b.getCenter().getX());
		double b2 = 2 * (a.getCenter().getY() - b.getCenter().getY());
		double c2 = 2 * (a.getRay() - b.getRay());
		double d2 = (Math.pow(a.getCenter().getX(), 2) + Math.pow(a.getCenter().getY(), 2) - Math.pow(a.getRay(), 2))
				- (Math.pow(b.getCenter().getX(), 2) + Math.pow(b.getCenter().getY(), 2) - Math.pow(b.getRay(), 2));

		double a3 = 2 * (a.getCenter().getX() - c.getCenter().getX());
		double b3 = 2 * (a.getCenter().getY() - c.getCenter().getY());
		double c3 = 2 * (a.getRay() - c.getRay());
		double d3 = (Math.pow(a.getCenter().getX(), 2) + Math.pow(a.getCenter().getY(), 2) - Math.pow(a.getRay(), 2))
				- (Math.pow(c.getCenter().getX(), 2) + Math.pow(c.getCenter().getY(), 2) - Math.pow(c.getRay(), 2));

		double aux = a2 * b3 - b2 * a3;
		double cx = (b3 * d2 - b2 * d3) / aux;
		double vx = (-b3 * c2 + b2 * c3) / aux;
		double cy = (-a3 * d2 + a2 * d3) / aux;
		double vy = (a3 * c2 - a2 * c3) / aux;
		
		double a4 = Math.pow(vx, 2) + Math.pow(vy, 2) - 1;
		double b4 = 2 * cx * vx - 2 * vx * a.getCenter().getX() + 2 * cy * vy - 2 * vy * a.getCenter().getY() - 2
				* a.getRay();
		double c4 = Math.pow(cx, 2) - 2 * cx * a.getCenter().getX() + Math.pow(a.getCenter().getX(), 2)
				+ Math.pow(cy, 2) - 2 * cy * a.getCenter().getY() + Math.pow(a.getCenter().getY(), 2)
				- Math.pow(a.getRay(), 2);

		Baskara baskara = new Baskara(a4, b4, c4);
		double r = baskara.getFirstRoot();		

		double x = cx + vx * r;
		double y = cy + vy * r;

		if(r < 0)
		{
			return null;
		}
		return new Disk(new Vertex(x, y), r);

	}

	// public static Disk getCircumCircles(Disk a, Disk b, Disk c)
	// {
	// Triplet<Disk> triplet = shrinkCircles(a, b, c);
	// Disk aux = triplet.getThird();
	// double r = getMinRay(a, b, c);
	//
	// Pair<Disk> pairWs = getWs(triplet);
	// Pair<Line> lines = getExternalTangents(pairWs);
	//
	// Disk c1 = transformationZ(lines.getFirst(), aux.getCenter());
	// Disk c2 = transformationZ(lines.getSecond(), aux.getCenter());
	//
	// c1.setRay(c1.getRay() - r);
	// c2.setRay(c2.getRay() - r);
	//
	// if (NumberPrimitives.isZero(c1.distance(a), 3) &&
	// NumberPrimitives.isZero(c1.distance(b), 3)
	// && NumberPrimitives.isZero(c1.distance(c), 3))
	// {
	//
	// return c1;
	// }
	// else if (NumberPrimitives.isZero(c2.distance(a), 3) &&
	// NumberPrimitives.isZero(c2.distance(b), 3)
	// && NumberPrimitives.isZero(c2.distance(c), 3))
	// {
	//
	// return c2;
	// }
	// else
	// {
	// return null;
	// }
	//
	// }

	private static double getMinRay(Disk a, Disk b, Disk c)
	{
		return Math.min(a.getRay(), Math.min(b.getRay(), c.getRay()));
	}

	private static Disk transformationZ(Line line, Vertex p)
	{

		// coeficiente angular
		double m = Math.tan(line.getAngle());

		double a, b;
		if (m == 0)
		{
			a = 0;
			b = -1 / line.getPoint().getY();
		}
		else if (Double.isInfinite(m))
		{
			a = -1 / line.getPoint().getX();
			b = 0;
		}
		else
		{
			a = -m / (m * line.getPoint().getX() - line.getPoint().getY());
			b = 1 / (m * line.getPoint().getX() - line.getPoint().getY());
		}

		return new Disk(new Vertex(-a / 2 + p.getX(), b / 2 + p.getY()), Math.sqrt(a * a + b * b) / 2);
	}

	/**
	 * Devolve as tangentes exteriores de dois c�rculos.
	 * 
	 * @param listWs
	 * @return
	 */
	public static Pair<Line> getExternalTangents(Pair<Disk> pairWs)
	{
		/*
		 * if (pairWs.getFirst().equals(pairWs.getSecond())) { throw new
		 * IllegalArgumentException("cannot find tangents, disks are equals"); }
		 * if
		 * (pairWs.getFirst().getCenter().equals(pairWs.getSecond().getCenter())) {
		 * throw new IllegalArgumentException("cannot find tangents, disks are
		 * concentric"); }
		 */

		if (pairWs.getFirst().getRay() < pairWs.getSecond().getRay())
		{
			pairWs = new Pair<Disk>(pairWs.getSecond(), pairWs.getFirst());
		}

		Disk bigDisk, smallDisk;

		double aux;

		Vertex point = new Vertex();

		bigDisk = pairWs.getFirst();
		smallDisk = pairWs.getSecond();

		double angleBetweenAxisXAndCrossingLine = getAngle(bigDisk, smallDisk);

		// raios diferentes
		if (!NumberPrimitives.isZero(bigDisk.getRay() - smallDisk.getRay()))
		{
			double distanceBetweenCenters = BasicPrimitives.distance(bigDisk.getCenter(), smallDisk.getCenter());
			double z = (smallDisk.getRay() * distanceBetweenCenters) / (bigDisk.getRay() - smallDisk.getRay());

			double angleBetweenCrossingLineAndTangents = Math.asin(bigDisk.getRay() / (z + distanceBetweenCenters));

			logger.debug(String.format("angle1 = %f angle2 = %f", Math.toDegrees(angleBetweenAxisXAndCrossingLine),
					Math.toDegrees(angleBetweenCrossingLineAndTangents)));
			logger.debug(String.format("distanceCenters = %f, z =  %f", distanceBetweenCenters, z));

			/* DEFININDO O PONTO COMUM DAS RETAS TANGENTES */
			double x = smallDisk.getCenter().getX();
			x += z * (smallDisk.getCenter().getX() - bigDisk.getCenter().getX()) / distanceBetweenCenters;
			point.setX(x);

			double y = smallDisk.getCenter().getY();
			y += z * (smallDisk.getCenter().getY() - bigDisk.getCenter().getY()) / distanceBetweenCenters;
			point.setY(y);
			logger.debug(String.format("point x = %f y = %f", point.getX(), point.getY()));
			/* DEFININDO O PONTO COMUM DAS RETAS TANGENTES */

			Line tangent1 = new Line(point, angleBetweenAxisXAndCrossingLine - angleBetweenCrossingLineAndTangents);
			Line tangent2 = new Line(point, angleBetweenAxisXAndCrossingLine + angleBetweenCrossingLineAndTangents);

			logger.debug(String.format("grauT1 = %f ", Math.toDegrees(angleBetweenAxisXAndCrossingLine
					- angleBetweenCrossingLineAndTangents)));
			logger.debug(String.format("grauT2 = %f ", Math.toDegrees(angleBetweenAxisXAndCrossingLine
					+ angleBetweenCrossingLineAndTangents)));
			return new Pair<Line>(tangent1, tangent2);
		}
		else
		{
			// raios iguais

			aux = Math.sin(angleBetweenAxisXAndCrossingLine - Math.PI / 2);
			double dy = aux * bigDisk.getRay();

			aux = Math.cos(angleBetweenAxisXAndCrossingLine - Math.PI / 2);
			double dx = aux * bigDisk.getRay();

			point.setY(bigDisk.getCenter().getY() + dy);
			point.setX(bigDisk.getCenter().getX() + dx);

			Line tangent1 = new Line(point, angleBetweenAxisXAndCrossingLine);

			logger.debug(String.format("dx = %f dy = %f", dx, dy));
			logger.debug(String.format("point x = %f y = %f", point.getX(), point.getY()));

			point = new Vertex(bigDisk.getCenter().getY() - dy, bigDisk.getCenter().getX() - dx);

			Line tangent2 = new Line(point, angleBetweenAxisXAndCrossingLine);

			logger.debug(String.format("point x = %f y = %f", point.getX(), point.getY()));
			return new Pair<Line>(tangent1, tangent2);
		}

	}

	/**
	 * Devolve o �ngulo entre a reta que passa pelos centros e o eixo x.
	 * 
	 * @param w1
	 * @param w2
	 * @return
	 */
	private static double getAngle(Disk w1, Disk w2)
	{
		double aux, angle;

		aux = w2.getCenter().getX() - w1.getCenter().getX();

		/* aux == 0 */
		if (NumberPrimitives.isZero(aux))
		{
			angle = Math.PI / 2;
		}
		else
		{
			aux = (w2.getCenter().getY() - w1.getCenter().getY()) / aux;
			angle = Math.atan(aux);

			/* angle1 < 0 */
			if (angle < 0 && NumberPrimitives.isZero(angle))
			{
				angle = -angle;
				angle = Math.PI - angle;
				logger.debug(String.format("ajustou o angle1 = %f ", Math.toDegrees(angle)));
			}
		}

		return angle;
	}

	/**
	 * Faz a transforma��o linear descrita no artigo "Voronoi diagram of
	 * circle set from Voronoi diagram of a point set: II. Geometry" para obter
	 * w1 e w2 e devolve-os em uma List.
	 * 
	 * @param list
	 * @return
	 */
	public static Pair<Disk> getWs(Triplet<Disk> triplet)
	{
		if (triplet.getFirst().getCenter().equals(triplet.getSecond().getCenter())
				|| triplet.getFirst().getCenter().equals(triplet.getSecond().getCenter()))
		{
			throw new IllegalArgumentException("disks have the same center");
		}

		if (triplet.getFirst().getRay() < triplet.getThird().getRay()
				|| triplet.getSecond().getRay() < triplet.getThird().getRay())
		{
			throw new IllegalArgumentException("third disk is not the smallest one");
		}

		double rayC1, rayC2, rayC3, p1, p2;
		Vertex centerC1, centerC2, centerC3, centerW1, centerW2;
		Disk w1, w2;

		centerC1 = triplet.getFirst().getCenter();
		centerC2 = triplet.getSecond().getCenter();
		centerC3 = triplet.getThird().getCenter();

		rayC1 = triplet.getFirst().getRay();
		rayC2 = triplet.getSecond().getRay();
		rayC3 = triplet.getThird().getRay();

		p1 = Math.pow(centerC1.getX() - centerC3.getX(), 2);
		p1 += Math.pow(centerC1.getY() - centerC3.getY(), 2);
		p1 -= Math.pow(rayC1 - rayC3, 2);

		p2 = Math.pow(centerC2.getX() - centerC3.getX(), 2);
		p2 += Math.pow(centerC2.getY() - centerC3.getY(), 2);
		p2 -= Math.pow(rayC2 - rayC3, 2);

		centerW1 = new Vertex();
		centerW1.setX((centerC1.getX() - centerC3.getX()) / p1);
		centerW1.setY(-((centerC1.getY() - centerC3.getY()) / p1));

		w1 = new Disk();
		w1.setCenter(centerW1);
		w1.setRay((rayC1 - rayC3) / p1);

		centerW2 = new Vertex();
		centerW2.setX((centerC2.getX() - centerC3.getX()) / p2);
		centerW2.setY(-((centerC2.getY() - centerC3.getY()) / p2));

		w2 = new Disk();
		w2.setCenter(centerW2);
		w2.setRay((rayC2 - rayC3) / p2);

		logger.debug(String.format("p1 = %f p2 = %f", p1, p2));
		logger.debug(String.format("W1 x = %f y = %f raio = %f", centerW1.getX(), centerW1.getY(), w1.getRay()));
		logger.debug(String.format("W2 x = %f y = %f raio = %f", centerW2.getX(), centerW2.getY(), w2.getRay()));

		// assert !w1.getCenter().equals(w2.getCenter());

		return new Pair<Disk>(w1, w2);
	}

	/**
	 * Faz copias (c1, c2 e c3) dos c�culos a, b, c. Tal que c1 � o de maior
	 * raio e c3 o de menor. Agora subtrai dos raios de c1, c2 e c3 o raio de c3
	 * (perceba que c3 agora � somente um ponto). Devolve c1, c2, c3 (nessa
	 * ordem) em uma List.
	 * 
	 * @param a
	 * @param b
	 * @param c
	 * @return
	 */
	public static Triplet<Disk> shrinkCircles(Disk a, Disk b, Disk c)
	{
		Triplet<Disk> triplet;

		if (a.getRay() > b.getRay())
		{
			if (a.getRay() > c.getRay())
			{
				if (c.getRay() > b.getRay())
				{
					triplet = new Triplet<Disk>(a.copy(), c.copy(), b.copy());
				}
				else
				{
					triplet = new Triplet<Disk>(a.copy(), b.copy(), c.copy());
				}
			}
			else
			{
				triplet = new Triplet<Disk>(c.copy(), a.copy(), b.copy());
			}
		}
		else
		{
			if (b.getRay() > c.getRay())
			{
				if (c.getRay() > a.getRay())
				{
					triplet = new Triplet<Disk>(b.copy(), c.copy(), a.copy());
				}
				else
				{
					triplet = new Triplet<Disk>(b.copy(), a.copy(), c.copy());
				}
			}
			else
			{
				triplet = new Triplet<Disk>(c.copy(), b.copy(), a.copy());
			}
		}

		triplet.getFirst().setRay(triplet.getFirst().getRay() - triplet.getThird().getRay());
		triplet.getSecond().setRay(triplet.getSecond().getRay() - triplet.getThird().getRay());
		triplet.getThird().setRay(0);

		logger.debug(String.format("C1 x = %f y = %f raio = %f", triplet.getFirst().getCenter().getX(), triplet
				.getFirst().getCenter().getY(), triplet.getFirst().getRay()));
		logger.debug(String.format("C2 x = %f y = %f raio = %f", triplet.getSecond().getCenter().getX(), triplet
				.getSecond().getCenter().getY(), triplet.getSecond().getRay()));
		logger.debug(String.format("C3 x = %f y = %f raio = %f", triplet.getThird().getCenter().getX(), triplet
				.getThird().getCenter().getY(), triplet.getThird().getRay()));

		return triplet;
	}

}
